package msi.agentClasses;

import java.util.HashMap;
import java.util.Set;
import java.util.SortedMap;

import msi.agentClasses.Location.Side;
import msi.someBasicWumpusApplication.Action;
import msi.someBasicWumpusApplication.TransferPercept;

import org.apache.log4j.Logger;

public class RulesDrivenAgent {
	private static Logger LOGGER = Logger.getLogger(RulesDrivenAgent.class);

	//states and qualities - there is some redundancy, to be pedantic
	private static final String reasonAboutShot = "REASON_ABOUT_SHOT";
	private static final String firedAnArrow = "FIRED_AN_ARROW";
	private static final String huntWumpus = "HUNT_THE_BEAST";
	private static final String foundWumpus = "FOUND_WUMPUS";
	private static final String killedWumpus = "KILLED_THE_BEAST";
	private static final String headHome = "HEAD_HOME";
	//private static final String haveGold = "I_GOT_THE_GOLD";
	private static final String obstacle = "IMPASSABLE";
	private static final String visited = "VISITED";
	private static final String safe = "SAFE";
	// hazards
	private static final String wumpus = "WUMPUS";

	private static final String stench = "STENCH";
	private static final String breeze = "BREEZE";
	private static final String glitter = "GLITTER";

	private static String progress(WumpusTile l1, WumpusTile l2) {
		return "PROGRESSED_FROM:" + l1.toString() + "to:" + l2.toString();
	}

	private HashMap<String, Boolean> knowledge;

	int wSize;
	Location loc = new Location();

	public RulesDrivenAgent(int worldSize) {
		knowledge = new HashMap<String, Boolean>();
		wSize = worldSize;
		initBorders();
	}

	/**
	 * Build up the knowledge about the edges of the map
	 */
	private void initBorders() {
		for (int x = 0; x < wSize + 2; ++x) {
			tell(obstacle + new WumpusTile(x, 0));
			tell(obstacle + new WumpusTile(x, wSize + 1));
			tell(obstacle + new WumpusTile(0, x));
			tell(obstacle + new WumpusTile(wSize + 1, x));
		}
	}

	/**
	 * Agent's machine state, sort of...
	 */
	public int handlePercept(TransferPercept tp) {
		if (ask(headHome)) {
			return headHome(tp);
		} else if (ask(huntWumpus)) {
			return huntWumpus(tp);
		} else if (ask(reasonAboutShot)) {
			return postBlindShotReasoning(tp);
		} else {
			return explore(tp);
		}
	}

	// called upon firing an arrow in arbitrary direction
	private int postBlindShotReasoning(TransferPercept tp) {
		if (tp.getScream()) {
			tell(killedWumpus);
		} else if (!tp.getBreeze()) {
			tell(safe + loc.getneighbor(Side.AHEAD));
		}
		tell(reasonAboutShot, false);
		return handlePercept(tp);
	}

	/**
	 * Retraces the steps to find stench and kill wumpus if needed and possible.
	 * If not possible, fires an arrow towards one of the possible wumpus' locations. 
	 */
	private int huntWumpus(TransferPercept tp) {
		//retrace steps till the stink hits you
		if (!tp.getStench()) {
			WumpusTile curTile = this.loc.getTile();
			SortedMap<Side, WumpusTile> area = this.loc.getSurroundings();
			for (Side side : area.keySet()) {
				if (ask(progress(area.get(side), curTile))) {
					if (side.equals(Side.AHEAD)) {
						tell(progress(area.get(side), curTile), false);
						loc.walk();
						return Action.GO_FORWARD;
					} else if (!side.equals(Side.RIGHT)) {
						loc.turn(Side.LEFT);
						return Action.TURN_LEFT;
					} else {
						loc.turn(Side.RIGHT);
						return Action.TURN_RIGHT;
					}
				}
			}
		} else {
			System.out.println("Here's wumpus. Get aim...");
			SortedMap<Side, WumpusTile> area = this.loc.getSurroundings();
			System.out.println(area.toString());
			for (Side side : area.keySet()) {
				if (ask(foundWumpus)) {
					if (ask(wumpus + area.get(side))) {
						// certain kill
						switch (side) {
						case AHEAD:
							System.out.println("Will kill");
							tell(huntWumpus, false);
							tell(firedAnArrow, true);
							return Action.SHOOT;
						case RIGHT:
							System.out.println("wumpus on the right");
							loc.turn(Side.RIGHT);
							return Action.TURN_RIGHT;
						default:
							loc.turn(Side.LEFT);
							return Action.TURN_LEFT;
						}
					}
				} else if (!ask(obstacle + area.get(side))
						&& !ask(safe + area.get(side))) {
					// shoot and check what happens
					switch (side) {
					case AHEAD:
						tell(huntWumpus, false);
						tell(firedAnArrow);
						tell(reasonAboutShot);
						return Action.SHOOT;
					case RIGHT:
						return Action.TURN_RIGHT;
					default:
						return Action.TURN_LEFT;
					}
				}
			}

		}
		System.out.println("Dunno whatta do. Climbing instead of hunting");
		return Action.CLIMB;
	}

	/**
	 * Retraces the steps taken by agent to find the entry point and leave the cave 
	 */
	private int headHome(TransferPercept tp) {
		WumpusTile curTile = this.loc.getTile();
		if (curTile.col == curTile.row && curTile.row == 1) {
			return Action.CLIMB;
		}
		SortedMap<Side, WumpusTile> area = this.loc.getSurroundings();
		for (Side side : area.keySet()) {
			if (ask(progress(area.get(side), curTile))) {
				if (side.equals(Side.AHEAD)) {
					tell(progress(area.get(side), curTile), false);
					loc.walk();
					return Action.GO_FORWARD;
				} else if (!side.equals(Side.RIGHT)) {
					loc.turn(Side.LEFT);
					return Action.TURN_LEFT;
				} else {
					loc.turn(Side.RIGHT);
					return Action.TURN_RIGHT;
				}
			}
		}
		System.out.println("Dunno whatta do. Climbing to get home");
		return Action.CLIMB;
	}

	/**
	 * In short: looks either for unvisited tiles or for tiles that have been
	 * entered from a tile other than the current one.
	 */

	private int explore(TransferPercept tp) {
		if (tp.getBump()) {
			this.tell(obstacle + loc.gps());
			this.tell(safe + loc.gps(), false);
			loc.bounceBack();
		} else {
			this.tell(visited + loc.gps());
			this.tell(safe + loc.gps());
		}
		if (tp.getBreeze()) {
			this.tell(breeze + loc.gps());
		}
		if (tp.getGlitter()) {
			this.tell(glitter + loc.gps());
			this.tell(headHome);
			return Action.GRAB;
		}
		if (tp.getScream()) {
			tell(killedWumpus);
		}
		if (tp.getStench()) {
			this.tell(stench + loc.gps());
		}
		if (!tp.getStench() && !tp.getBreeze()) {
			for (WumpusTile wt : loc.getTile().getAdjacentTiles(wSize)) {
				tell(safe + wt);
			}
		} else {
			ruleSomeOut();
		}

		System.out.println("Percept examined");

		SortedMap<Side, WumpusTile> area = this.loc.getSurroundings();
		for (int i = 0; i < 2; ++i) {
			for (Side s : area.keySet()) {
				WumpusTile tile = area.get(s);
				String query = null;
				boolean value = true;
				switch (i) {
				case 0:
					query = visited + tile;
					value = false;
					break;
				case 1:
					query = progress(this.loc.getTile(), tile);
					value = false;
					break;
				default:
					break;
				}

				System.out.println("Current criterion: " + query);
				if (ask(safe + tile) && askIf(obstacle + tile, false)
						&& askIf(query, value)) {
					if (s.equals(Side.AHEAD)) {
						tell(progress(this.loc.getTile(), tile));
						loc.walk();
						return Action.GO_FORWARD;
					} else if (!s.equals(Side.RIGHT)) {
						loc.turn(Side.LEFT);
						return Action.TURN_LEFT;
					} else {
						loc.turn(Side.RIGHT);
						return Action.TURN_RIGHT;
					}
				}
			}
		}
		System.out.println("Nothing to, nowhere to go. Heading home");
		if (this.ask(foundWumpus) && !this.ask(killedWumpus) && !this.ask(firedAnArrow)) {
			this.tell(huntWumpus);
			return huntWumpus(tp);
		} else {
			if (!ask(firedAnArrow)) {
				tell(huntWumpus);
				return huntWumpus(tp);
			} else {
				this.tell(headHome);
				return this.headHome(tp);
			}
		}
	}

	/**
	 * Reasoning about the world. For each examined tile (neighbors of the
	 * current tile) determines the intersection of percepts describing it.
	 * Should it be empty, tile is considered safe. Once two or more tiles
	 * suggest a presence of wumpus in considered tile, it is marked as occupied
	 * by wumpus Once the wumpus is found, all the other tiles threatened only
	 * by him are labeled safe. Finding wumpus launches the procedure of safe
	 * paths removal from the history, so that they can be reentered
	 */
	private void ruleSomeOut() {
		Set<WumpusTile> immediateNeighbors = loc.getTile().getAdjacentTiles(
				wSize);
		for (WumpusTile immNbr : immediateNeighbors) {
			if (!this.ask(visited + immNbr) && !this.ask(safe + immNbr)) {
				Set<WumpusTile> transitiveNeighbors = immNbr
						.getAdjacentTiles(wSize);
				boolean reachable = false;
				Boolean wumpus = null;
				Boolean pit = null;
				int observations = 0;
				for (WumpusTile trNbr : transitiveNeighbors) {
					if (this.ask(visited + trNbr)) {
						reachable = true;
						observations++;
						if (wumpus == null && pit == null) {
							wumpus = ask(stench + trNbr);
							pit = ask(breeze + trNbr);
						} else {
							wumpus = wumpus && ask(stench + trNbr);
							pit = pit && ask(breeze + trNbr);
						}
					}
				}
				if (reachable && !wumpus && !pit) {
					tell(safe + immNbr);
				} else if (reachable && wumpus && observations > 1
						&& !ask(foundWumpus)) {
					tell(RulesDrivenAgent.wumpus + immNbr);
					tell(RulesDrivenAgent.foundWumpus);
					retractPaths(immNbr);
				} else if (reachable && !pit && wumpus && ask(foundWumpus)
						&& !ask(RulesDrivenAgent.wumpus + immNbr)) {
					tell(safe + immNbr);
				}
			}
		}
	}

	/**
	 * Strores sentence with given value 
	 */
	public void tell(String msg, boolean value) {
		knowledge.put(msg, value);
		LOGGER.info("TOLD: " + msg + ": " + value);
	}

	/**
	 * Stores sentence as true 
	 */
	public void tell(String msg) {
		tell(msg, true);
	}

	/**
	 * 
	 * @param question
	 * @param value
	 * @return true if the answer stored for question equals value, false if
	 *         values differ (lack of knowledge in the database is considered as
	 *         false)
	 */
	public boolean askIf(String question, boolean value) {
		return ask(question) == value;
	}

	public boolean ask(String question) {
		boolean result = (knowledge.containsKey(question) ? knowledge
				.get(question) : false);
		LOGGER.info("ASK: " + question + " - " + result);
		return result;
	}

	/**
	 * Establishing the location of wumpus enables the agent to reenter some
	 * tiles which he previously considered too risky. To comply with general
	 * rules they should therefore be erased from the progressions' history.
	 * 
	 * Subject to removal will be the history of visits entries reflecting
	 * moving back and forth within the wumpus' vicinity. Optionally, we can
	 * recursively follow that 'two-way' path till the first fork.
	 */
	private void retractPaths(WumpusTile t) {
		Set<WumpusTile> nbrs = t.getAdjacentTiles(wSize);
		for (WumpusTile nbr : nbrs) {
			Set<WumpusTile> potentialOrigins = nbr.getAdjacentTiles(wSize);
			for (WumpusTile po : potentialOrigins) {
				if (ask(progress(nbr, po)) && ask(progress(po, nbr))) {
					tell(progress(nbr, po), false);
					tell(progress(po, nbr), false);
					tell(visited + nbr, false);
					retractPaths(po);
				}
			}
		}
	}
}
